using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Reactor.Extensions;
using Reactor.ServiceGrid.FileSystem;
using Reactor.ServiceGrid.Packages;

namespace Reactor.ServiceGrid.Repositories
{
    public class LocalPackageRepository : PackageRepositoryBase
    {
        #region Fields

        private readonly Dictionary<string, PackageCacheEntry> _packageCache = new Dictionary<string, PackageCacheEntry>(StringComparer.OrdinalIgnoreCase);

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="LocalPackageRepository"/> class.
        /// </summary>
        /// <param name="physicalPath">The physical path.</param>
        public LocalPackageRepository(string physicalPath) : this(new DefaultPackagePathResolver(physicalPath), new PhysicalFileSystem(physicalPath)) {}

        /// <summary>
        /// Initializes a new instance of the <see cref="LocalPackageRepository"/> class.
        /// </summary>
        /// <param name="pathResolver">The path resolver.</param>
        /// <param name="fileSystem">The file system.</param>
        public LocalPackageRepository(IPackagePathResolver pathResolver, IFileSystem fileSystem)
        {
            if (pathResolver == null)
                throw new ArgumentNullException("pathResolver");

            if (fileSystem == null)
                throw new ArgumentNullException("fileSystem");

            FileSystem = fileSystem;
            PathResolver = pathResolver;

            if(Log.IsDebugEnabled) Log.DebugFormat("Repository created. Root path: {0}", fileSystem.Root);
        }

        #endregion

        #region Properties

        public override string Source
        {
            get { return FileSystem.Root; }
        }

        protected IFileSystem FileSystem { get; private set; }

        private IPackagePathResolver PathResolver { get; set; }

        #endregion

        #region Methods

        public override IQueryable<IPackage> GetPackages()
        {
            var packages = GetPackagesEnumerable();
            return packages.AsSafeQueryable();
        }

        public override void AddPackage(IPackage package)
        {
            var packageFilePath = GetPackageFilePath(package);

            FileSystem.AddFileWithCheck(packageFilePath, package.GetStream);
        }

        public override void RemovePackage(IPackage package)
        {
            // Delete the package file
            var packageFilePath = GetPackageFilePath(package);
            FileSystem.DeleteFileSafe(packageFilePath);

            // Delete the package directory if any
            FileSystem.DeleteDirectorySafe(PathResolver.GetPackageDirectory(package), recursive: false);

            // If this is the last package delete the package directory
            if (!FileSystem.GetFilesSafe(String.Empty).Any() && !FileSystem.GetDirectoriesSafe(String.Empty).Any())
                FileSystem.DeleteDirectorySafe(String.Empty, recursive: false);
        }

        private IEnumerable<IPackage> GetPackagesEnumerable()
        {
            var packages = new List<IPackage>();
            foreach (var path in GetPackageFiles())
            {
                PackageCacheEntry cacheEntry;
                var lastModified = FileSystem.GetLastModified(path);
                // If we never cached this file or we did and it's current last modified time is newer
                // create a new entry
                if (!_packageCache.TryGetValue(path, out cacheEntry) || (cacheEntry != null && lastModified > cacheEntry.LastModifiedTime))
                {
                    // We need to do this so we capture the correct loop variable
                    string packagePath = path;

                    // Create the package
                    IPackage package = OpenPackage(packagePath);

                    // create a cache entry with the last modified time
                    cacheEntry = new PackageCacheEntry(package, lastModified);

                    // Store the entry
                    _packageCache[packagePath] = cacheEntry;
                }

                packages.Add(cacheEntry.Package);
            }

            return packages.ToArray();
        }

        internal IEnumerable<string> GetPackageFiles()
        {
            // Check for package files one level deep. We use this at package install time
            // to determine the set of installed packages. Installed packages are copied to 
            // {id}.{version}\{packagefile}.{extension}.
            foreach (var dir in FileSystem.GetDirectories(String.Empty))
            {
                foreach (var path in FileSystem.GetFiles(dir, "*" + Constants.PackageExtension))
                {
                    yield return path;
                }
            }

            // Check top level directory
            foreach (var path in FileSystem.GetFiles(String.Empty, "*" + Constants.PackageExtension))
            {
                yield return path;
            }
        }

        #endregion

        protected virtual IPackage OpenPackage(string path)
        {
            return new ZipPackage(() => FileSystem.OpenFile(path));
        }

        private string GetPackageFilePath(IPackage package)
        {
            return Path.Combine(PathResolver.GetPackageDirectory(package), PathResolver.GetPackageFileName(package));
        }

        private class PackageCacheEntry
        {
            public PackageCacheEntry(IPackage package, DateTimeOffset lastModifiedTime)
            {
                Package = package;
                LastModifiedTime = lastModifiedTime;
            }

            public IPackage Package { get; private set; }
            public DateTimeOffset LastModifiedTime { get; private set; }
        }
    }
}
